{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "# <center>第12章 Python Web 开发技术</center>\n",
    "\n",
    "<br>\n",
    "\n",
    "- [12.1 通用网关接口(CGI)](#12.1-通用网关接口(CGI))\n",
    "  - [12.1.1 CGI 的概念](#12.1.1-CGI-的概念)\n",
    "  - [12.1.2 Python CGI 编程](#12.1.2-Python-CGI-编程)\n",
    "- [12.2 Web 服务器网关接口(WSGI)](#12.2-Web-服务器网关接口(WSGI))\n",
    "  - [12.2.1 WSGI 的概念](#12.2.1-WSGI-的概念)\n",
    "  - [12.2.2 WSGI 应用](#12.2.2-WSGI-应用)\n",
    "  - [12.2.3 WSGI 服务器](#12.2.3-WSGI-服务器)\n",
    "  - [12.2.4 示例](#12.2.4-示例)\n",
    "- [12.3 异步服务器网关接口(ASGI)*](#12.3-异步服务器网关接口(ASGI)*)\n",
    "  - [12.3.1 ASGI 应用](#12.3.1-ASGI-应用)\n",
    "  - [12.3.2 HTTP 子协议](#12.3.2-HTTP-子协议)\n",
    "  - [12.3.3 ASGI 服务器](#12.3.3-ASGI-服务器)\n",
    "  - [12.3.4 示例](#12.3.4-示例)\n",
    "- [12.4 Web 应用框架](#12.4-Web-应用框架)\n",
    "  - [12.4.1 Web 框架的基本概念](#12.4.1-Web-框架的基本概念)\n",
    "  - [12.4.2 WSGI 框架](#12.4.2-WSGI-框架)\n",
    "  - [12.4.3 ASGI 框架 *](#12.4.3-ASGI-框架-*)\n",
    "- [12.5 Web 开发中的设计模式](#12.5-Web-开发中的设计模式)\n",
    "  - [12.5.1 MVC 模式](#12.5.1-MVC-模式)\n",
    "  - [12.5.2 MVC 模式的变种](#12.5.2-MVC-模式的变种)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "## 12.1 通用网关接口(CGI)\n",
    "\n",
    "### 12.1.1 CGI 的概念\n",
    "\n",
    "- 直接基于Socket的Web应用的问题\n",
    "   - 要求熟悉HTTP协议和Socket编程\n",
    "   - 程序的可扩展性很差\n",
    "   - 难以支撑复杂的业务逻辑\n",
    "- 通用网关接口（Common Gateway Interface，CGI）\n",
    "   - 是一种Web服务器和Web应用程序之间交互的编程规范，定义了Web服务器如何向Web应用程序发送消息，在收到Web应用程序的信息后如何处理等细节\n",
    "   - 遵循CGI规范的应用程序独立于Web服务器，并能够被Web服务器调用\n",
    "   - Web应用程序的开发者只需要关注业务逻辑代码的编写，无需关注HTTP协议或Socket等的底层细节\n",
    "   - 大大降低了Web应用程序的开发难度\n",
    "   - 通过标准输入输出进行通信，因而可以使用任意编程语言实现"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "<center>\n",
    "  <img src=\"./figures/fig12-1.png\" width=\"50%\"/>\n",
    "</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "- CGI编程的不足之处\n",
    "  - 在每一次HTTP请求中，Web服务器都需要执行CGI脚本，难以满足较大量并发请求的需要；\n",
    "  - CGI编程依旧比较复杂，相当多的交互依旧发生在HTTP协议的层面之上；\n",
    "  - CGI程序的安全性较差，容易受到攻击"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "### 12.1.2 Python CGI 编程\n",
    "\n",
    "- Web应用程序目录\n",
    "\n",
    "```\n",
    "www ─┬── server.py\n",
    "     ├─  index.html\n",
    "     └── cgi-bin ──── first_cgi.py\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "- 服务器（两种方式）\n",
    "  - server.py\n",
    "\n",
    "  ```python\n",
    "  # 例12-1\n",
    "  from http.server import HTTPServer, CGIHTTPRequestHandler\n",
    "\n",
    "  port = 9000\n",
    "  server = HTTPServer(('127.0.0.1', port), CGIHTTPRequestHandler)\n",
    "  print('服务器启动 http://{}:{}'.format(*server.server_address))\n",
    "\n",
    "  server.serve_forever()\n",
    "  ```\n",
    "  - 终端命令\n",
    "  `python -m http.server --cgi 9000`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "- CGI脚本\n",
    "  - 必须具有可执行权限，在Linux系统中可使用命令`chmod a+x cgi.py`添加执行权限；\n",
    "  - 脚本中首注释中给出Python解释器的路径，例如`#!/usr/bin/python`\n",
    "  - first_cgi.py\n",
    "\n",
    "```python\n",
    "# 例 12-2\n",
    "```\n",
    "\n",
    "```python\n",
    "#!/usr/bin/python\n",
    "# 该路径需根据Python环境进行设置\n",
    "\n",
    "print(\"Content-type: text/html\")                     # 响应头\n",
    "print(\"\\r\\n\")                                        # 空行\n",
    "\n",
    "print('''\n",
    "<html>\n",
    "<head>\n",
    "    <title>第一个CGI程序</title>\n",
    "</head>\n",
    "<body>\n",
    "    <center><h1>第一个CGI程序</h1></center>\n",
    "</body>\n",
    "</html>\n",
    "''')\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "<br>\n",
    "<center>\n",
    "  <img src=\"./figures/fig12-3.png\" width=\"80%\"/>\n",
    "</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "#### 在 CGI 脚本中获取请求参数\n",
    "\n",
    "- 代码\n",
    "  - index.html中表单的`action`属性改为`/cgi-bin/post.py`\n",
    "  - post.py\n",
    "\n",
    "```python\n",
    "# 例 12-3\n",
    "```\n",
    "\n",
    "```python\n",
    "#!/path/to/envs/env_name/bin/python\n",
    "# 该路径需根据Python环境进行设置\n",
    "import cgi\n",
    "\n",
    "print(\"Content-type: text/html\")\n",
    "\n",
    "print('\\r\\n')\n",
    "\n",
    "fields = cgi.FieldStorage()\n",
    "username = fields.getvalue('usertag')\n",
    "code = fields.getvalue('code')\n",
    "print('<html><head><title>作业提交</title></head>')\n",
    "\n",
    "if username is None and code is None:\n",
    "    print(\"<center><h1>请求失败，参数错误！</h1></center>\")\n",
    "else:\n",
    "    with open('./index.html') as f:\n",
    "        html = f.read()\n",
    "        html = html.replace('<span id=\"info\"/>', f'{username} 提交成功! <br>{code}')\n",
    "        print(html)\n",
    "print('</html>')\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "<br>\n",
    "<center>\n",
    "  <img src=\"./figures/fig12-4.png\" width=\"50%\"/>\n",
    "</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "#### 使用Cookie\n",
    "\n",
    "- cookie的发送\n",
    "  - 通过http头部来实现\n",
    "- `Set-cookie:name=value;expires=date;path=path;domain=domain;secure`\n",
    "  - `name=value`：cookie变量的名称和取，有多个cookie变量时用 `;` 分隔，如`name1=value1;name2=value2`；\n",
    "  - `expires=date`：cookie的有效期限，date的格式为`Wdy,DD-Mon-YYYY HH:MM:SS`；\n",
    "  - `path=path`：cookie的支持路径，cookie对该路径中所有文件及子目录生效，例如`path=/cgi-bin`；\n",
    "  - `domain=domain`：cookie生效的域名，例如：`domain=\"www.example.com\"`；\n",
    "  - `secure`：表示cookie只能通过安全套接字协议（SSL）来传递。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "\n",
    "- cookie的接收\n",
    "  - 通过环境变量HTTP_COOKIE\n",
    "\n",
    "\n",
    "```python\n",
    "# 例 12-4\n",
    "```\n",
    "\n",
    "```python\n",
    "#!/path/to/envs/env_name/bin/python\n",
    "# 该路径需根据Python环境进行设置\n",
    "import cgi\n",
    "import os\n",
    "import http.cookies\n",
    "\n",
    "form = cgi.FieldStorage()\n",
    "username = form.getvalue('usertag')\n",
    "code = form.getvalue('code')\n",
    "\n",
    "print(\"Content-type: text/html\")\n",
    "print(f\"Set-Cookie: code={code}\")                 # 设置Cookie项\n",
    "print('\\r\\n')\n",
    "\n",
    "old_code = None\n",
    "sc = http.cookies.SimpleCookie()                  # Cookie解析器\n",
    "sc.load(os.environ.get('HTTP_COOKIE'))\n",
    "old_code = sc.get('code', None)\n",
    "if old_code: old_code = old_code.value            # 获取Cookie项code的值\n",
    "\n",
    "if username is None or code is None:\n",
    "    print(\"<center><h1>请求失败，参数错误！</h1></center>\")\n",
    "else:\n",
    "    with open('./index.html') as f:\n",
    "        html = f.read()\n",
    "        info = f'{username} 提交成功! <br>{code}'\n",
    "        if old_code:\n",
    "            info = info + f'<br>上次提交的代码为:<br>{old_code}'\n",
    "        html = html.replace('<span id=\"info\"/>', info)\n",
    "        print(html)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "## 12.2 Web 服务器网关接口(WSGI)\n",
    "\n",
    "### 12.2.1 WSGI 的概念\n",
    "\n",
    "- CGI编程的根本问题\n",
    "  - 每次请求都需要调用CGI脚本创建新的进程，并且响应完毕之后即抛弃\n",
    "- 解决办法\n",
    "  - 第一种称为服务器集成，其思路是利用Web服务器提供的API开发Web应用，然后将Web应用作为模块插入到Web服务器中，Web服务器通过函数调用的方式来执行Web应用程序\n",
    "    - 要求Web应用与Web服务器使用相同的编程语言，而且Web服务器的稳定性直接受到Web应用程序的影响\n",
    "  - 第二种思路是外部进程，即令Web应用程序独立于Web服务器，形成独立的外部应用程序\n",
    "    - 需要制定新的专用于Web服务器和外部进程之间的通信协议，例如Java语言中的Servelet和Python语言中的WSGI（Web Server Gateway Interface）"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "### 12.2.2 WSGI 应用\n",
    "\n",
    "- 满足如下条件的程序就是一个合法的WSGI应用\n",
    "  - 是一个可调用对象；\n",
    "  - 能够接收两个参数\n",
    "    - 第一个是包含了Web服务器环境变量的字典\n",
    "    - 第二个是一个用于设置HTTP响应状态码和响应头的函数\n",
    "  - 返回一个用于构造HTTP响应体的可迭代对象\n",
    "\n",
    "```python\n",
    "# 例 12-5\n",
    "```\n",
    "\n",
    "```python\n",
    "def wsgi_app(env, start_response):\n",
    "    content = '<h1>第一个WSGI应用</h1>'.encode('utf-8')\n",
    "    status = '200 OK'\n",
    "    headers = [('Content-Type', 'text/html'),\n",
    "               ('Content-Length', str(len(content)))]\n",
    "    start_response(status, headers)\n",
    "    return [content]\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "- WSGI应用的运行\n",
    "  - WSGI应用由WSGI服务器（或称为WSGI容器）调用，它就是所谓的外部进程，负责Web服务器和WSGI应用之间的通信交互\n",
    "  \n",
    "- WSGI的运行过程\n",
    "\n",
    "<center>\n",
    "  <img src=\"./figures/fig12-5.png\" width=\"55%\"/>\n",
    "</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "### 12.2.3 WSGI 服务器\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "\n",
    "- 简易WSGI服务器\n",
    "\n",
    "```python\n",
    "# 例 12-6\n",
    "```\n",
    "\n",
    "```python\n",
    "# 文件 wsgi_server.py\n",
    "from socket import socket\n",
    "from httplib import Parser\n",
    "import threading, sys, io\n",
    "\n",
    "class WSGIServer:\n",
    "    def __init__(self, app, host='127.0.0.1', port=9000):\n",
    "        self.app = app\n",
    "        self.host = host\n",
    "        self.port = port\n",
    "        self.resp_status = None\n",
    "        self.resp_headers = None\n",
    "\n",
    "    def start(self):\n",
    "        server_socket = socket()\n",
    "        print(f\"启动服务器，http://{self.host}:{self.port}\")\n",
    "        server_socket.bind((self.host, self.port))\n",
    "        server_socket.listen(10)\n",
    "        while True:                                        # 服务循环\n",
    "            conn, addr = server_socket.accept()\n",
    "            try:\n",
    "                conn.settimeout(60)\n",
    "                print(f\"客户端：{addr}\")\n",
    "                threading.Thread(target=self.server_thread,# 服务线程\n",
    "                                 args=(conn,)).start()\n",
    "            except Exception:\n",
    "                print('服务器发生错误！')\n",
    "\n",
    "    def start_response(self, status, headers):\n",
    "        self.resp_status, self.resp_headers = status, headers\n",
    "\n",
    "    def server_thread(self, conn):\n",
    "        parser = Parser()\n",
    "        while True:                                        # 通信循环\n",
    "            parser.append(conn.recv(1024))\n",
    "            if parser.is_ok():\n",
    "                break\n",
    "        env = self.make_env(parser)                        # 构造环境变量\n",
    "        app_contents = self.app(env, self.start_response)  # 调用WSGI应用\n",
    "        resp_text = self.make_head(self.resp_status, self.resp_headers)\n",
    "        response = resp_text.encode('utf-8')\n",
    "        response += b' '.join(app_contents)\n",
    "        conn.send(response)                                # HTTP响应\n",
    "        conn.close()\n",
    "\n",
    "    def make_head(self, status, headers):                  # 构造响应头\n",
    "        head = f'HTTP/1.1 {status}\\r\\n'\n",
    "        for k, v in headers:\n",
    "            head += f'{k}: {v}\\r\\n'\n",
    "        head += '\\r\\n'\n",
    "        return head\n",
    "\n",
    "    def make_env(self, parser):                            # 环境变量字典\n",
    "        return {\n",
    "            'wsgi.version': (1, 0),\n",
    "            'wsgi.url_scheme': 'http',\n",
    "            'wsgi.input': io.BytesIO(parser.body),\n",
    "            'wsgi.errors': sys.stderr,\n",
    "            'wsgi.multithread': True,\n",
    "            'wsgi.multiprocess': False,\n",
    "            'wsgi.run_once': False,\n",
    "            'REQUEST_METHOD': parser.req_method,\n",
    "            'SCRIPT_NAME': '',\n",
    "            'PATH_INFO': parser.req_url,\n",
    "            'CONTENT_TYPE': parser.content_type,\n",
    "            'CONTENT_LENGTH': parser.content_length,\n",
    "            'SERVER_NAME': self.host,\n",
    "            'SERVER_PORT': self.port,\n",
    "            'QUERY_STRING': parser.query_string,\n",
    "            'SERVER_PROTOCOL': 'HTTP 1.1',\n",
    "            'HTTP_COOKIE': parser.head_dict.get('cookie', ''),\n",
    "            'params': parser.req_params()\n",
    "        }\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    from wsgi_apps import first_wsgi_app\n",
    "    server = WSGIServer(first_wsgi_app)\n",
    "    server.start()\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "- Python内置的WSGI参考服务器\n",
    "\n",
    "```python\n",
    "# 例 12-7\n",
    "```\n",
    "\n",
    "```python\n",
    "from wsgiref.simple_server import make_server\n",
    "from apps import wsgi_app\n",
    "\n",
    "host = '127.0.0.1'\n",
    "port = 9000\n",
    "print(f\"启动服务器，http://{host}:{port}\\n\")\n",
    "server = make_server(host, port, wsgi_app)\n",
    "server.serve_forever()\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "#### 生产环境中常用的WSGI服务器\n",
    "\n",
    "- 常用的WSGI服务器有`uWSGI`、`Gunicorn`、` CherryPy`、`Tornado`、`Paste`、`Waitress`等\n",
    "\n",
    "- `Waitress`是一个纯Python的WSGI 服务器，安装使用非常方便，\n",
    "  - `pip install waitress`\n",
    "  \n",
    "```python\n",
    "import waitress\n",
    "from apps import wsgi_app\n",
    "waitress.serve(wsgi_app, listen='127.0.0.1:9000')\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "### 12.2.4 示例\n",
    "\n",
    "```python\n",
    "# 例 12-8\n",
    "```\n",
    "\n",
    "```python\n",
    "from urllib.parse import parse_qs\n",
    "import http.cookies\n",
    "\n",
    "def wsgi_app(env, start_response):\n",
    "    method = env['REQUEST_METHOD']                        # 请求方法\n",
    "    query = env['QUERY_STRING']                           # 请求参数\n",
    "    if not query and method == 'POST':\n",
    "        content_len = int(env['CONTENT_LENGTH'])\n",
    "        query = env['wsgi.input'].read(content_len).decode('utf-8')\n",
    "\n",
    "    with open('index.html') as f:                         # 读取HTML文档\n",
    "        content = f.read()\n",
    "\n",
    "    headers = [('Content-Type', 'text/html')]             # 响应头\n",
    "    info = ''\n",
    "    if method == 'POST':                                  # 响应POST请求\n",
    "        # 处理请求参数\n",
    "        params = parse_qs(query)\n",
    "        username = params.get('usertag')\n",
    "        code = params.get('code', '')\n",
    "        if username:\n",
    "            username = username[0]\n",
    "        if code:\n",
    "            code = code[0]\n",
    "\n",
    "        # 从cookie中读取上次提交的code，并将本次提交的code写入cookie\n",
    "        cookie_str = env.get('HTTP_COOKIE', '')\n",
    "        sc = http.cookies.SimpleCookie()\n",
    "        sc.load(cookie_str)\n",
    "        old_code = sc['code'].value if sc.get('code') else ''\n",
    "        info = f'{username}提交成功!<br>{code}'\n",
    "        if old_code:\n",
    "            info = info+f'<br>上次提交的代码为:<br>{old_code}'\n",
    "        headers.append(('Set-Cookie', f'code={code}'))    # 写入cookie\n",
    "\n",
    "    content = content.replace('<span id=\"info\"/>', info).encode('utf-8')\n",
    "    headers.append(('Content-Length', str(len(content)))) # 请求正文长度\n",
    "    start_response('200 OK', headers)\n",
    "    return [content]\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    # 1.-- WSGIServer\n",
    "    from wsgi_server import WSGIServer\n",
    "    server = WSGIServer(wsgi_app)\n",
    "    server.start()\n",
    "\n",
    "#     # 2.-- simple_server\n",
    "#     from wsgiref.simple_server import make_server\n",
    "#     print(\"启动服务器，http://127.0.0.1:9000\")\n",
    "#     server = make_server('127.0.0.1', 9000, wsgi_app)\n",
    "#     server.serve_forever()\n",
    "\n",
    "#     # 3.-- waitress\n",
    "#     import waitress\n",
    "#     waitress.serve(wsgi_app, listen='127.0.0.1:9000'\n",
    "\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "## 12.3 异步服务器网关接口(ASGI)*\n",
    "\n",
    "- 异步服务器网关接口(Asynchronous Server Gateway Interface, ASGI)是 WSGI 的 “精神继承者”\n",
    "  - 提供了对异步编程的支持\n",
    "  - 为新型协议或标准提供了扩展的可能性\n",
    "  - 在一定程度上兼容 WSGI"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "### 12.3.1 ASGI 应用\n",
    "\n",
    "- 典型的ASGI应用\n",
    "\n",
    "```python\n",
    "async def application(scope, receive, send):\n",
    "    receive_event = await receive()\n",
    "    ...\n",
    "    await send(send_event\n",
    "```\n",
    "- 参数说明\n",
    "  - `scope`：是一个字典，其中包含了服务器与客户端之间连接的详细信息（键和取值的格式在子协议中规定），其作用与WSGI应用中的环境变量参数`env`相似\n",
    "  - `receive`：是一个异步可等待对象，用于接收来自客户端的消息，用`await`语句异步运行后返回一个事件（Event），其中包含了客户端的消息内容\n",
    "  - `send`：也是一个异步可等待对象，用于向客户端发送消息，用`await`语句异步运行时以一个事件作为参数，其中包含了需要发送至客户端的消息内容\n",
    "\n",
    "- 事件\n",
    "  - 字典，其键和取值由不同的子协议来确定\n",
    "  - HTTP 子协议\n",
    "  - WebSocket 子协议\n",
    "  - Lifespan 子协议"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "### 12.3.2 HTTP 子协议\n",
    "\n",
    "#### `scope`字典\n",
    "\n",
    "- `type`：`\"http\"`\n",
    "- `asgi[\"version\"]`：ASGI 协议版本，当前最新版本为`\"3.0\"`\n",
    "- `asgi[\"spec_version\"]`：HTTP 子协议版本，取值为`\"2.0\"` 或`\"2.1\"`，默认为`\"2.0\"`\n",
    "- `http_version`：`\"1.0\"`、`\"1.1\"` 或`\"2\"`\n",
    "- `method`：HTTP 请求方法(大写形式)\n",
    "- `scheme`：协议名，取值为 `\"http\"` 或` \"https\"`\n",
    "- `path`：HTTP 请求路径字符串(不包括查询串部分)\n",
    "- `raw_path`：HTTP 请求头部路径部分的原始字节串，默认取值为 `None`\n",
    "- `query_string`：查询字符串\n",
    "- `root_path`：ASGI 应用的根路径，与 WSGI 中 SCRIPT_NAME 的含义相同，默认取 值为空字符串\n",
    "- `headers`：HTTP 请求头，取值为形如 `[[header_name, header_value]]` 的可迭 代对象，`header_name`和`header_value`都是字节串，请求头的顺序与其在 HTTP 请 求中的出现顺序一致\n",
    "- `client`：客户端地址，取值为 `[host, port]`，默认为 `None`\n",
    "- `server`：服务器地址，取值为` [host, port] `或者` [sock, None]`(`sock` 为套接字对象)，默认为 `None`\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "#### 请求事件\n",
    "\n",
    "- 请求事件是`receive`的异步调用返回结果，在 ASGI 应用主要用于获取 HTTP 请求正文，常用的键和取值\n",
    "  - `type`: 取值为`\"http.request\"`\n",
    "  - `body`: HTTP 请求正文字节串，默认取值为`b\"\"`，当请求正文较大时可以分块处理\n",
    "  - `more_body`: 取值为`True`或`False`，当取值为`True`时表示请求正文分块传输，并且还存在后续数据块，WSGI 应用应当等待所有数据块接收完毕后合并处理\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "#### 响应开始事件\n",
    "\n",
    "- 与 WSGI 应用中的start_response相似，表示开始构建 HTTP 响应\n",
    "  - `type`: 取值为`\"http.response.start\"`\n",
    "  - `status`: HTTP 响应状态码(整数)\n",
    "  - `headers`: HTTP 响应头，取值为形如`[[header_name, header_value]]`的可迭代对象，`header_name`和`header_value`都是字节串，响应头的顺序与在 HTTP 响应 中的出现顺序一致"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 响应正文事件\n",
    "- 包含了 HTTP 响应正文信息\n",
    "  - `type`: 取值为`\"http.response.body\"`\n",
    "  - `body`: HTTP 响应正文字节串，默认取值为`b\"\"`，响应正文也可以分块处理\n",
    "  - `more_body`:取值为`True`或`False`，当取值为`True`时表示还存在后续块\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "### 12.3.3 ASGI 服务器\n",
    "\n",
    "```python\n",
    "# 例12-9\n",
    "```\n",
    "\n",
    "```python\n",
    "# 文件 asgi_server.py\n",
    "import asyncio\n",
    "from httplib import Parser\n",
    "\n",
    "class ASGIServer:\n",
    "    def __init__(self, app, host='127.0.0.1', port=9000):\n",
    "        self.app = app\n",
    "        self.host = host\n",
    "        self.port = port\n",
    "\n",
    "    async def __asgi_server(self, reader, writer):\n",
    "        print(writer.get_extra_info('peername'))\n",
    "        parser = Parser()\n",
    "        msgs_receive = asyncio.Queue()           # HTTP请求事件消息队列\n",
    "        while True:                              # 接收客户端HTTP请求\n",
    "            data_recv = await reader.read(1024)\n",
    "            parser.append(data_recv)\n",
    "            if parser.is_ok(): break\n",
    "        msg = self.make_msg(parser)              # 构造请求事件消息\n",
    "        await msgs_receive.put(msg)\n",
    "        scope = self.make_scope(parser)\n",
    "        msgs_send = asyncio.Queue()              # HTTP响应事件消息队列\n",
    "        await self.app(scope, msgs_receive.get,  # 异步调用ASGI应用\n",
    "                       msgs_send.put)\n",
    "        while True:                              # 处理HTTP响应事件\n",
    "            msg = await msgs_send.get()\n",
    "            if not self.send_msg(msg, writer): break\n",
    "        await writer.drain()\n",
    "        writer.close()\n",
    "\n",
    "    def make_msg(self, parser):                   # 构造请求事件字典\n",
    "        return { \"type\": \"http.request\",\n",
    "                 \"body\": parser.body,\n",
    "                 \"more_body\": False}\n",
    "\n",
    "    def make_scope(self, parser):                 # 构造scope字典\n",
    "        headers = [[name.encode('utf-8'), value.encode('utf-8')]\n",
    "                   for name, value in parser.head_dict.items()]\n",
    "        return { \"type\": \"http\",\n",
    "                 \"method\": parser.req_method,\n",
    "                 \"scheme\": \"http\",\n",
    "                 \"raw_path\": parser.req_url.encode(),\n",
    "                 \"query_string\": parser.query_string,\n",
    "                 \"path\": parser.path,\n",
    "                 \"headers\": headers}\n",
    "\n",
    "    def send_msg(self, msg, writer):\n",
    "        if msg[\"type\"] == \"http.response.start\":  # 处理响应开始事件\n",
    "            writer.write(b\"HTTP/1.1 %d\\r\\n\" % msg[\"status\"])\n",
    "            for header in msg[\"headers\"]:\n",
    "                writer.write(b\"%s: %s\\r\\n\" % (header))\n",
    "            writer.write(b\"\\r\\n\")\n",
    "        if msg[\"type\"] == \"http.response.body\":  # 处理响应正文事件\n",
    "            writer.write(msg[\"body\"])\n",
    "            return msg.get(\"more_body\", False)\n",
    "        return True\n",
    "\n",
    "    def start(self):\n",
    "        print(f\"启动服务器，http://{self.host}:{self.port}\")\n",
    "        async def main():\n",
    "            server = await asyncio.start_server(self.__asgi_server,\n",
    "                                                self.host, self.port)\n",
    "            await server.serve_forever()\n",
    "        asyncio.run(main())\n",
    "```\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "### 12.3.4 示例\n",
    "\n",
    "```python\n",
    "# 例12-10\n",
    "```\n",
    "\n",
    "```python\n",
    "from urllib.parse import parse_qs\n",
    "from http import cookies\n",
    "\n",
    "async def asgi_app(scope, receive, send):\n",
    "    method = scope['method']                        # 请求方法\n",
    "    code = ''\n",
    "    info = ''\n",
    "    if method == 'POST':\n",
    "        event = await receive()                     # 接收请求事件\n",
    "        # 处理请求参数\n",
    "        query = event['body'].decode('utf-8')\n",
    "        params = parse_qs(query)\n",
    "        username = params.get('usertag')\n",
    "        code = params.get('code', '')\n",
    "        if username:\n",
    "            username = username[0]\n",
    "        if code:\n",
    "            code = code[0]\n",
    "        # 从cookie中读取上次提交的code，并将本次提交的code写入cookie\n",
    "        cookie_str = dict(scope[\"headers\"]).get('cookie', '')\n",
    "        sc = cookies.SimpleCookie()\n",
    "        sc.load(cookie_str)\n",
    "        old_code = sc['code'].value if sc.get('code') else ''\n",
    "        info = f'{username}提交成功!<br>{code}'\n",
    "        if old_code:\n",
    "            info = info+f'<br>上次提交的代码为:<br>{old_code}'\n",
    "\n",
    "    with open('./index.html') as f:\n",
    "        body = f.read()\n",
    "    body = body.replace('<span id=\"info\"/>', info).encode('utf-8')\n",
    "    await send({                                    # 发送响应开始事件\n",
    "        \"type\": \"http.response.start\",\n",
    "        \"status\": 200,\n",
    "        \"headers\": [\n",
    "            (b\"Content-Length\", b\"%d\" % len(body)),\n",
    "            (b\"Content-Type\", b\"text/html\"),\n",
    "            (b\"Set-Cookie\", f'code={code}'.encode('utf-8')),\n",
    "        ],\n",
    "    })\n",
    "    await send({                                    # 发送响应正文事件\n",
    "        \"type\": \"http.response.body\",\n",
    "        \"body\": body,\n",
    "    })\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    # 1. -- ASGIServer\n",
    "    from asgi_server import ASGIServer\n",
    "    server = ASGIServer(asgi_app)\n",
    "    server.start()\n",
    "\n",
    "#     # 2. -- Uvicorn\n",
    "#     import uvicorn\n",
    "#     uvicorn.run(asgi_app, host=\"127.0.0.1\", port=9000)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "## 12.4 Web 应用框架\n",
    "\n",
    "\n",
    "### 12.4.1 Web 框架的基本概念\n",
    "\n",
    "- Web 框架\n",
    "  - Web 框架对 Web 应用程序开发过程中的通用部分进行了抽象和封装，使得 Web 应用程序开发者不必关心底层的实现细节，大大降低了开发的难度和成本\n",
    "  - Python 中的 Web 框架本质上就是一个 WSGI 应用或 ASGI 应用\n",
    "- Web 框架的其他功能\n",
    "  - 安全问题\n",
    "  - URL路由\n",
    "  - Cookie使用\n",
    "  - 会话管理\n",
    "  - 模板引擎\n",
    "  - 数据库访问\n",
    "  - ... ..."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "<center>\n",
    "  <img src=\"./figures/fig12-6.png\" width=\"65%\"/>\n",
    "</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "- 常见 WSGI 框架\n",
    "  - Flask、Django、Tornado、Bottle等，\n",
    "- 常见ASGI 框架\n",
    "  - FastAPI、Django Channels、Starlette"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "### 12.4.2 WSGI 框架\n",
    "\n",
    "- 工具（AGSI框架也会用到）\n",
    "\n",
    "```python\n",
    "# 例12-11\n",
    "```\n",
    "\n",
    "```python\n",
    "# 文件 framwork_utils.py\n",
    "from pathlib import Path\n",
    "\n",
    "def router(path, method, handlers):            # URL路由装饰器\n",
    "    def decorater(handler):\n",
    "        handlers[method][path] = handler\n",
    "        return handler\n",
    "    return decorater\n",
    "\n",
    "def load_static(static_path):                  # 加载静态文件\n",
    "    statci_dict = dict()\n",
    "    for f_type in ['html', 'js', 'css']:\n",
    "        for file in Path(static_path).glob(f'*.{f_type}'):\n",
    "            with open(file) as f:\n",
    "                statci_dict[file.name] = f.read()\n",
    "    return statci_dict\n",
    "\n",
    "page404 = '''<html><head><title>404</title></head><body>\n",
    "        <center><br><h1>404: page note found</h1></center>\n",
    "        </body></html>'''\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 简易WSGI框架\n",
    "\n",
    "```python\n",
    "# 例12-12\n",
    "```\n",
    "\n",
    "```python\n",
    "# 文件wsgi_framework.py\n",
    "from urllib.parse import parse_qs\n",
    "from http.cookies import SimpleCookie\n",
    "from framwork_utils import *\n",
    "\n",
    "class WSGIFramework:\n",
    "    def __init__(self, static='.'):\n",
    "        self.handlers = {'GET': dict(), 'POST': dict()}\n",
    "        self.statics = load_static(static)\n",
    "\n",
    "    def route(self, path, method):                      # URL路由装饰器\n",
    "        assert method in ['GET', 'POST'], '方法必须是GET或POST'\n",
    "        return router(path, method, self.handlers)\n",
    "\n",
    "    def __call__(self, env, start_response):            # WSGI应用\n",
    "        method = env['REQUEST_METHOD']                  # 请求方法\n",
    "        path = env['PATH_INFO']                         # 请求路径\n",
    "        cookie_str = env['HTTP_COOKIE']                 # Cookies\n",
    "        query = env['QUERY_STRING']\n",
    "        req_obj = None\n",
    "        if method == 'POST':\n",
    "            size = int(env.get('CONTENT_LENGTH', 0))\n",
    "            query = env['wsgi.input'].read(size).decode('utf-8')\n",
    "            req_obj = parse_qs(query)                   # 处理请求参数\n",
    "            sc = SimpleCookie()\n",
    "            sc.load(cookie_str)                         # 处理Cookie\n",
    "            req_obj['cookies'] = {k:m.value for k, m in sc.items()}\n",
    "        handler = self.handlers[method].get(path, False)# 获取handler\n",
    "        ck_dict = None\n",
    "        if not handler:\n",
    "            status = '404 Not Found'\n",
    "            content = page404.encode('utf-8')\n",
    "        else:\n",
    "            status = '200 OK'\n",
    "            content = handler(req_obj)                  # 执行handler\n",
    "            if isinstance(content, tuple):\n",
    "                content, ck_dict = content\n",
    "            content = content.encode('utf-8')\n",
    "        headers = [('Content-Type', 'text/html'),\n",
    "                   ('Content-Length', str(len(content)))]\n",
    "        if ck_dict:\n",
    "            set_ck = ';'.join([f'{k}={v}' for k, v in ck_dict.items()])\n",
    "            headers.append(('Set-Cookie', set_ck))\n",
    "        start_response(status, headers)\n",
    "        return [content]\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "- WSGI Web 框架的应用\n",
    "\n",
    "```python\n",
    "# 例12-13\n",
    "```\n",
    "\n",
    "```python\n",
    "from wsgi_framework import WSGIFramework\n",
    "app = WSGIFramework()\n",
    "\n",
    "@app.route('/', 'GET')\n",
    "def index_get(_):\n",
    "    content = app.statics['index.html']\n",
    "    return content\n",
    "\n",
    "@app.route('/', 'POST')\n",
    "def index_post(request):\n",
    "    content = app.statics['index.html']\n",
    "    username = request.get('usertag')\n",
    "    code = request.get('code')\n",
    "    old_code = request.get('cookies').get('code', '')\n",
    "    if username:\n",
    "        username = username[0]\n",
    "    if code:\n",
    "        code = code[0]\n",
    "    info = f'{username}提交成功!<br>{code}'\n",
    "    if old_code:\n",
    "        info = info+f'<br>上次提交的代码为:<br>{old_code}'\n",
    "    content = content.replace(r'<span id=\"info\"/>', info)\n",
    "    return content, {'code':code}\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    # 1. -- WSGIServer\n",
    "    from wsgi_server import WSGIServer\n",
    "    server = WSGIServer(app)\n",
    "    server.start()\n",
    "\n",
    "#     # 2. -- waitress\n",
    "#     import waitress\n",
    "#     waitress.serve(app, listen='127.0.0.1:9000')\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "#### Flask\n",
    "\n",
    "Flask是一个著名的“微型”Python Web开发框架\n",
    "- “微型”指的是Flask仅保持一个小而简单但是足够强大、可扩展性良好的核心\n",
    "- 默认情况下Flask仅包含基本的Web请求响应以及URL路由等核心功能，不过利用丰富的扩展可以非常方便地添加数据库对象关系映射、模板引擎、表单验证等功能，从而打造与项目相适应的Web开发框架\n",
    "- 安装\n",
    "  - `pip install flask`\n",
    "- Flask依赖于Werkzeug和 Jinja2\n",
    "  - Werkzeug是一个WSGI的工具包实现了WSGI的底层功能如请求/响应对象对象的构建、URL路由、环境配置等\n",
    "  - Jinja2是一个模板引擎"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "##### （1）基本应用\n",
    "\n",
    "```python\n",
    "from flask import Flask\n",
    "app = Flask(__name__)\n",
    "\n",
    "@app.route('/')\n",
    "def hello_web():\n",
    "    return \"Hello Web\"\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    app.run(host='127.0.0.1', port=9000, debug=True)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "##### （2）URL路由\n",
    "\n",
    "- 两种方式：\n",
    "  - `add_url_rule`方法\n",
    "  - `route`装饰器\n",
    "\n",
    "```python\n",
    "from flask import Flask\n",
    "app = Flask(__name__)\n",
    "\n",
    "def hello_web():\n",
    "    return '''\n",
    "    <html>\n",
    "      <head><title>Flast Test</title></head>\n",
    "      <body><center><h1>Hello Web</h1><center></body>\n",
    "    </html>'''\n",
    "\n",
    "app.add_url_rule('/', view_func=hello_web)\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    app.run(host='127.0.0.1', port=9000, debug=True)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "- URL变量处理\n",
    "\n",
    "```python\n",
    "from flask import Flask\n",
    "app = Flask(__name__)\n",
    "\n",
    "@app.route('/<user>')\n",
    "def hello_web(user):\n",
    "    return f'''\n",
    "    <html>\n",
    "      <head><title>Flast Test</title></head>\n",
    "      <body><center><h1>Hello {user}</h1><center></body>\n",
    "    </html>'''\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    app.run(host='127.0.0.1', port=9000, debug=True)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "- URL变量转换器\n",
    "\n",
    "```python\n",
    "from flask import Flask\n",
    "app = Flask(__name__)\n",
    "\n",
    "@app.route('/<user>/<int:uid>')\n",
    "def hello_web(user, uid):\n",
    "    return f'''\n",
    "    <html>\n",
    "      <head><title>Flast Test</title></head>\n",
    "      <body><center><h1>Hello {user}, your id is {uid}</h1><center></body>\n",
    "    </html>'''\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    app.run(host='127.0.0.1', port=9000, debug=True)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "- 常用变量转换器\n",
    "  - int转换器 `<int:param>` ：接收整数\n",
    "  - float转换器 `<float:param>`: 接收浮点数\n",
    "  - string转换器 `<string:param>`: 接收string类型（默认转换器）\n",
    "  - path转换器 `<path:param>`:和默认的相似，但也接收斜线\n",
    "  - 自定义转换器（继承`werkzeug.routing.BaseConverter`，添加到字典`app.url_map.converters`之中）"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "##### （3）`request`对象\n",
    "- `request`对象的属性\n",
    "  - `method `：当前请求方法；\n",
    "  - `Form `：包含表单参数及其值的字典；\n",
    "  - `args `：查询字符串中的参数；\n",
    "  - `Cookies`：Cookie字典；\n",
    "  - `files `：文件上传的相关数据。\n",
    "\n",
    "```python\n",
    "from flask import Flask, request\n",
    "app = Flask(__name__)\n",
    "\n",
    "@app.route('/', methods=['GET', 'POST'])\n",
    "def hello_web():\n",
    "    f = open('./templates/index.html')\n",
    "    html = f.read()\n",
    "    f.close()\n",
    "    if request.method == 'GET':\n",
    "        return html.replace('{{info|safe}}', '')\n",
    "    else:\n",
    "        username = request.form.get('usertag')\n",
    "        code = request.form.get('code')\n",
    "        return html.replace('{{info|safe}}', f'提交成功<br>用户名：{username}<br>代码：<br>{code}')\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    app.run(host='127.0.0.1', port=9000, debug=True)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "##### （4）模板\n",
    "\n",
    "```python\n",
    "from flask import Flask, request, render_template\n",
    "app = Flask(__name__, template_folder='templates', static_folder='static', static_url_path='/static')\n",
    "\n",
    "\n",
    "@app.route('/', methods=['GET', 'POST'])\n",
    "def index():\n",
    "    if request.method == 'GET':\n",
    "        return render_template('index.html')\n",
    "    username = request.form['usertag']\n",
    "    code = request.form['code']\n",
    "    print(code)\n",
    "    return render_template('index.html', info=f'提交成功<br>用户名：{username}<br>代码：<br>{code}')\n",
    "\n",
    "\n",
    "app.run(debug=True, use_reloader=True, host='127.0.0.1', port=9000)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "- jinja2\n",
    "  - 控制结构` {% %}`\n",
    "  - 变量取值` {{ }}`\n",
    "  - 注释` {# #}`\n",
    "  - for循环\n",
    "\n",
    "  ```html\n",
    "  <ul>\n",
    "  {% for user in users %}\n",
    "  <li>{{user.username}}</li>\n",
    "  {% endfor %}\n",
    "  </ul>\n",
    "  ```\n",
    "  - 过滤器\n",
    "    - `safe`：不对字符串进行转义；\n",
    "    - `capitialize`：字符串首字母大写；\n",
    "    - `lower`：字符串小写；\n",
    "    - `upper`：字符串大写；\n",
    "    - `title`：单词首字母大写；\n",
    "    - `trim`：去除字符串两端空格；\n",
    "    - `striptags`：去除字符串中所有HTML；\n",
    "    - `join`：拼接多个值为字符串；\n",
    "    - `replace`：字符串替换；\n",
    "    - `round`：四舍五入；\n",
    "    - `int`：字符串转整型。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "##### （5）静态文件处理\n",
    "\n",
    "- 静态文件主要包括图片、视频、javascript、css等，其特点是数量较多、不会发生变化、不需要Python代码介入\n",
    "\n",
    "```python\n",
    "from flask import Flask, request, render_template\n",
    "app = Flask(__name__, template_folder='templates', static_folder='static', static_url_path='/static')\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "##### （6）Cookie与Session\n",
    "\n",
    "- Cookie\n",
    "\n",
    "```python\n",
    "from flask import Flask, request, render_template, make_response\n",
    "app = Flask(__name__, template_folder='templates', static_folder='static', static_url_path='/static')\n",
    "\n",
    "@app.route('/', methods=['GET', 'POST'])\n",
    "def index():\n",
    "    if request.method == 'GET':\n",
    "        return render_template('static.html')\n",
    "    \n",
    "    username = request.form['usertag']\n",
    "    code = request.form['code']\n",
    "    old_code = request.cookies.get('code')\n",
    "\n",
    "    content = render_template('static.html',\n",
    "                           info=f'提交成功<br>用户名：{username}<br>代码：<br>{code}<br>上一次提交的代码：<br>{old_code}'\n",
    "                           )\n",
    "    response = make_response(content)\n",
    "    response.set_cookie(\"code\", code)\n",
    "    return response\n",
    "\n",
    "\n",
    "app.run(debug=True, use_reloader=True, host='127.0.0.1', port=9000)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "- Session\n",
    "\n",
    "```python\n",
    "from flask import Flask, request, render_template, session\n",
    "app = Flask(__name__, template_folder='templates', static_folder='static', static_url_path='/static')\n",
    "app.secret_key = 'keykey'\n",
    "\n",
    "@app.route('/', methods=['GET', 'POST'])\n",
    "def index():\n",
    "    if request.method == 'GET':\n",
    "        return render_template('static.html')\n",
    "\n",
    "    username = request.form['usertag']\n",
    "    password = request.form['password']\n",
    "    code = request.form['code']\n",
    "    user = session.get('user')\n",
    "    print(user)\n",
    "    if user:\n",
    "        info = f'提交成功<br>用户名：{user}<br>代码：<br>{code}'\n",
    "    elif (username == 'admin' and password == '123456'):\n",
    "        info = f'提交成功<br>用户名：{username}<br>代码：<br>{code}'\n",
    "        session['user'] = username\n",
    "    else:\n",
    "        info = f'用户名和密码不正确！'\n",
    "\n",
    "    return render_template('static.html', info=info)\n",
    "\n",
    "@app.route('/logout')\n",
    "def logout():\n",
    "    if session.get('user'):\n",
    "        del session['user']\n",
    "    return render_template('static.html')\n",
    "\n",
    "app.run(debug=True, use_reloader=True, host='127.0.0.1', port=9000)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "#####  (7) 拦截器\n",
    "\n",
    "```python\n",
    "@app.before_request\n",
    "def intercept():\n",
    "    print(f\"拦截 {request.path}, {request.method}\")\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "#####  (8) 部署\n",
    "\n",
    "\n",
    "\n",
    "- Nginx配置\n",
    "\n",
    "```json\n",
    "        location / {\n",
    "            proxy_pass http://127.0.0.1:9000;\n",
    "        }\n",
    "        location /static {\n",
    "            alias /Users/liuchen/Desktop/Python编程及应用/code/web/server-flask/static;\n",
    "            access_log off;\n",
    "            expires 10d;\n",
    "            break;\n",
    "        }\n",
    "```\n",
    "\n",
    "- WSGI服务器\n",
    "  - 端口为9000\n",
    "\n",
    "- Ningx命令\n",
    "  - `nginx`：启动ningx\n",
    "  - `nginx -s reload`：重新加载nginx配置\n",
    "  - `nginx -s stop`：关闭nginx"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "### 12.4.3 ASGI 框架 *\n",
    "\n",
    "- 简易 ASGI Web 框架\n",
    "\n",
    "```python\n",
    "# 例 12-14\n",
    "```\n",
    "\n",
    "```python\n",
    "# 文件asgi_framework.py\n",
    "from urllib.parse import parse_qs\n",
    "from http.cookies import SimpleCookie\n",
    "from framwork_utils import *\n",
    "\n",
    "class ASGIFramework:\n",
    "    def __init__(self, static='.'):\n",
    "        self.handlers = {'GET': dict(), 'POST': dict()}\n",
    "        self.statics = load_static(static)\n",
    "\n",
    "    def route(self, path, method):                      # URL路由装饰器\n",
    "        assert method in ['GET', 'POST'], '方法必须是GET或POST'\n",
    "        return router(path, method, self.handlers)\n",
    "\n",
    "    async def __call__(self, scope, receive, send):     # ASGI应用\n",
    "        method = scope['method']                        # 请求方法\n",
    "        path = scope['raw_path'].decode('utf-8')        # 请求路径\n",
    "        cookie_str = dict(scope[\"headers\"]).get(b'cookie', '') # Cookies\n",
    "        req_obj = None\n",
    "        if method == 'POST':\n",
    "            event = await receive()                     # 接收请求事件\n",
    "            query = event['body'].decode('utf-8')\n",
    "            req_obj = parse_qs(query)                   # 处理请求参数\n",
    "            sc = SimpleCookie()\n",
    "            sc.load(cookie_str.decode('utf-8'))         # 处理Cookie\n",
    "            req_obj['cookies'] = {k: m.value for k, m in sc.items()}\n",
    "        handler = self.handlers[method].get(path, False)# 获取handler\n",
    "        ck_dict = None\n",
    "        if not handler:\n",
    "            status = 404\n",
    "            content = page404.encode('utf-8')\n",
    "        else:\n",
    "            status = 200\n",
    "            content = await handler(req_obj)            # 执行handler\n",
    "            if isinstance(content, tuple):\n",
    "                content, ck_dict = content\n",
    "            content = content.encode('utf-8')\n",
    "        headers = [\n",
    "            (b\"Content-Length\", b\"%d\" % len(content)),\n",
    "            (b\"Content-Type\", b\"text/html\"),\n",
    "        ]\n",
    "        if ck_dict:\n",
    "            set_ck = ';'.join([f'{k}={v}' for k, v in ck_dict.items()])\n",
    "            headers.append((b'Set-Cookie', set_ck.encode('utf-8')))\n",
    "        await send({                                    # 发送响应开始事件\n",
    "            \"type\": \"http.response.start\",\n",
    "            \"status\": status,\n",
    "            \"headers\": headers\n",
    "        })\n",
    "        await send({                                    # 发送响应正文事件\n",
    "            \"type\": \"http.response.body\",\n",
    "            \"body\": content,\n",
    "        })\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "- ASGI Web 框架的应用\n",
    "\n",
    "```python\n",
    "# 例12-15\n",
    "```\n",
    "\n",
    "```python\n",
    "from asgi_framework import ASGIFramework\n",
    "app = ASGIFramework()\n",
    "\n",
    "@app.route('/', 'GET')\n",
    "async def index_get(_):\n",
    "    content = app.statics['index.html']\n",
    "    return content\n",
    "\n",
    "@app.route('/', 'POST')\n",
    "async def index_post(request):\n",
    "    content = app.statics['index.html']\n",
    "    username = request.get('usertag')\n",
    "    code = request.get('code')\n",
    "    old_code = request.get('cookies').get('code', '')\n",
    "    if username:\n",
    "        username = username[0]\n",
    "    if code:\n",
    "        code = code[0]\n",
    "    info = f'{username}提交成功!<br>{code}'\n",
    "    if old_code:\n",
    "        info = info+f'<br>上次提交的代码为:<br>{old_code}'\n",
    "    content = content.replace(r'<span id=\"info\"/>', info)\n",
    "    return content, {'code':code}\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    # 1. -- ASGIServer\n",
    "    from asgi_server import ASGIServer\n",
    "    server = ASGIServer(app)\n",
    "    server.start()\n",
    "\n",
    "#     # 2. -- Uvicorn\n",
    "#     import uvicorn\n",
    "#     uvicorn.run(app, host=\"127.0.0.1\", port=9000)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "## 12.5 Web 开发中的设计模式\n",
    "\n",
    "### 12.5.1 MVC 模式\n",
    "\n",
    "MVC是一种软件的开发模式，将软件的业务逻辑、数据与界面显示相分离，从而降低它们之间的耦合性，以便于代码的重用、重构与维护。\n",
    "\n",
    "- 模型（Model）：负责封装应用的业务逻辑数据及相关操作方法，例如数据库的查询、更新等操作；\n",
    "- 视图（View）：负责呈现业务逻辑的处理结果；\n",
    "- 控制器（Controller）：负责控制应用的业务流程，对模型和视图进行组织、整合。\n",
    "\n",
    "<center>\n",
    "  <img src=\"./figures/fig12-7.png\" width=\"40%\"/>\n",
    "</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "- MVC的优势\n",
    "  - 开发效率高\n",
    "  - 可维护性好\n",
    "  - 代码复用度高\n",
    "  - 便于设计\n",
    "  - 利于实现工具化和自动化\n",
    "\n",
    "- Web 开发中的 MVC 模式\n",
    "  - HTTP 协议的请求/响应访问模式使得模型不能主动向视图发送消息\n",
    "\n",
    "<center>\n",
    "  <img src=\"./figures/fig12-8.png\" width=\"45%\"/>\n",
    "</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "### 12.5.2 MVC 模式的变种\n",
    "\n",
    "- MTV（Model-Template-View）\n",
    "  - 模型:也称为数据存取层，负责数据库的访问与数据验证等\n",
    "  - 模板:也称为表现层，负责页面和数据的显示\n",
    "  - 视图:也称为业务逻辑层，根据业务逻辑调用模型获取业务逻辑数据，然后将业务逻辑数据与模板结合生成响应信息\n",
    "\n",
    "<center>\n",
    "  <img src=\"./figures/fig12-9.png\" width=\"45%\"/>\n",
    "</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "\n",
    "- MVP（Model-View-Presenter）\n",
    "  - 模型\n",
    "  - 视图\n",
    "  - Presenter：MVC 模式中的控制器，不过它主要负责业务逻辑处理\n",
    "\n",
    "<center>\n",
    "  <img src=\"./figures/fig12-10.png\" width=\"55%\"/>\n",
    "</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "- MVVM（Model-View-ViewModel）\n",
    "  - Model\n",
    "  - View\n",
    "  - ViewModel\n",
    "  - 视图与 ViewModel 之间的数据绑定\n",
    "\n",
    "<center>\n",
    "  <img src=\"./figures/fig12-11.png\" width=\"55%\"/>\n",
    "</center>"
   ]
  }
 ],
 "metadata": {
  "author": "liuchen",
  "celltoolbar": "幻灯片",
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.1"
  },
  "rise": {
   "enable_chalkboard": true,
   "footer": "",
   "progress": true,
   "scroll": true,
   "slideNumber": true
  },
  "toc": {
   "base_numbering": 1,
   "nav_menu": {},
   "number_sections": false,
   "sideBar": true,
   "skip_h1_title": false,
   "title_cell": "Table of Contents",
   "title_sidebar": "Contents",
   "toc_cell": false,
   "toc_position": {},
   "toc_section_display": true,
   "toc_window_display": false
  },
  "url": "https://github.com/hitlic/python_book"
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
